home *** CD-ROM | disk | FTP | other *** search
/ PC Professionell 2006 June / PCpro_2006_06.ISO / files / freeware / openvip.exe / {app} / TimelineWidget.py < prev    next >
Encoding:
Python Source  |  2003-06-08  |  44.2 KB  |  1,179 lines

  1. #
  2. # This file is part of OpenVIP (http://openvip.sourceforge.net)
  3. #
  4. # Copyright (C) 2002-2003
  5. # Michal Dvorak, Jiri Sedlar, Antonin Slavik, Vaclav Slavik, Jozef Smizansky
  6. #
  7. # This program is licensed under GNU General Public License version 2;
  8. # see file COPYING in the top level directory for details.
  9. #
  10. # $Id: TimelineWidget.py,v 1.57 2003/06/08 10:42:48 vaclavslavik Exp $
  11. #
  12. # Widget that displays data represented by model.Timeline
  13. #
  14.  
  15. import os.path
  16. import model, globals, openvip, notify
  17. import worker, threading
  18. from wxPython.wx import *
  19.  
  20. TRACK_HEIGHT     = 32
  21. RULER_HEIGHT     = 20
  22. BACKGROUND_COLOR = wxColour(0xFF,0xFF,0xDD)
  23. HEADER_COLOR     = wxColour(0xD9,0xDC,0xF6)
  24. HEADER_WIDTH     = 50
  25. VSCROLL_STEP     = TRACK_HEIGHT
  26. HSCROLL_STEP     = VSCROLL_STEP
  27.  
  28. SNAP_TOLERANCE   = 3 #pixels
  29. RESIZE_TOLERANCE = 3 #pixels
  30.  
  31. # Number of tracks displayed:
  32. NUM_TRACKS       = 5
  33.  
  34. STD_VIDEO_TRACKS = ['VA', 'VFx', 'VB'] + \
  35.                    [('V%i' % i) for i in range(0,NUM_TRACKS)]
  36. STD_AUDIO_TRACKS = ['AA', 'AFx', 'AB'] + \
  37.                    [('A%i' % i) for i in range(0,NUM_TRACKS)]
  38.  
  39. THUMBNAIL_HEIGHT = TRACK_HEIGHT - 3
  40.  
  41. ADDITIONAL_SPACE_ON_RIGHT = 0.25
  42.  
  43. # widget modes:
  44. MODE_FAST          = 1
  45. MODE_ONE_THUMBNAIL = 2
  46. MODE_THUMBNAILS    = 3
  47.  
  48. # mouse action modes:
  49.  
  50. # Mode for selecting, dragging and resizing objects:
  51. MOUSE_MODE_NORMAL  = 1
  52. # Mode for cutting objects into pieces:
  53. MOUSE_MODE_CUT     = 2
  54. # Mode for adding new objects:
  55. MOUSE_MODE_ADD     = 3
  56.  
  57.  
  58. if sys.platform == 'win32': fontFace = 'Arial'
  59. else:                       fontFace = ''
  60. fontNormal = wxFont(8, wxDEFAULT, wxNORMAL, wxNORMAL, faceName=fontFace)
  61. fontTiny = wxFont(7, wxDEFAULT, wxNORMAL, wxNORMAL, faceName=fontFace)
  62.  
  63. def sec2timecode(sec):
  64.     """Converts seconds to timecode in form H:MM:SS."""
  65.     h = int(sec / 3600)
  66.     m = int((sec - h * 3600) / 60)
  67.     s = int(sec - h * 3600 - m * 60)
  68.     return '%i:%02i:%02i' % (h,m,s)
  69.  
  70. wxEVT_UPDATE_THUMBNAILS = wxNewEventType()
  71.  
  72. def EVT_UPDATE_THUMBNAILS(win, func):
  73.     win.Connect(-1, -1, wxEVT_UPDATE_THUMBNAILS, func)
  74.  
  75. class UpdateThumbnailsEvent(wxPyEvent):
  76.     """This event is generated when new thumbnail is available."""
  77.     def __init__(self):
  78.         wxPyEvent.__init__(self)
  79.         self.SetEventType(wxEVT_UPDATE_THUMBNAILS)
  80.  
  81.  
  82. # maximum number of thumbnails kept in memory - the cache is purged
  83. # when exceeded:
  84. MAX_THUMBS = 200
  85.  
  86. # number of thumbnails in cache:
  87. thumbsCnt = 0
  88.  
  89.  
  90. class Thumbnail:
  91.     """Helper class that represents generated thumbnail for a clip."""
  92.  
  93.     NOTHING = 0
  94.     MAKING  = 1
  95.     IMAGE   = 2
  96.     BITMAP  = 3
  97.  
  98.     class Generator:
  99.         """This is a function that generates thumbnails on background"""
  100.         def __init__(self, thumb, obj, mode, zoom, windows):
  101.             self.thumb = thumb
  102.             self.obj = obj
  103.             self.mode = mode
  104.             self.zoom = zoom
  105.             self.index = 0
  106.             self.bitmaps = []
  107.             self.windows = windows
  108.  
  109.         def __notify(self):
  110.             for wnd in self.windows:
  111.                 evt = UpdateThumbnailsEvent()
  112.                 evt.SetEventObject(wnd)
  113.                 wxPostEvent(wnd, evt)
  114.  
  115.         def run(self):
  116.             file = self.obj.src_spec['filename']
  117.             try:
  118.                 fi = globals.get_file_info(file)
  119.             except openvip.Error:
  120.                 wxLogError("Can't get information about file '%s'" % file)
  121.                 return
  122.             for s in fi.video_streams:
  123.                 if s.name == self.obj.src_channel:
  124.                     self.thumb.size = (s.width * THUMBNAIL_HEIGHT / s.height,
  125.                                        THUMBNAIL_HEIGHT)
  126.                     pixels=int((self.obj.time_to-self.obj.time_from)*self.zoom)
  127.                     if self.mode == MODE_ONE_THUMBNAIL:
  128.                         frames = 3
  129.                         self.thumb.count = 1
  130.                     else:
  131.                         frames = (pixels / self.thumb.size[0]) + 1
  132.                         self.thumb.count = frames
  133.                     self.bitmaps = [ (Thumbnail.NOTHING,None)
  134.                                      for i in range(0,frames) ]
  135.                     self.thumb.bitmaps = self.bitmaps
  136.                     self.g = globals.core.create_thumbnails_generator(file,
  137.                                 s.name, self.thumb.size[0], self.thumb.size[1],
  138.                                 frames, openvip.DestCallback(None))
  139.                     self.thumb.ready = True
  140.                     self.__notify()
  141.                     return
  142.             for s in fi.audio_streams:
  143.                 if s.name == self.obj.src_channel:                    
  144.                     self.thumb.size = (THUMBNAIL_HEIGHT, THUMBNAIL_HEIGHT)
  145.                     pixels=int((self.obj.time_to-self.obj.time_from)*self.zoom)
  146.                     if self.mode == MODE_THUMBNAILS:
  147.                         frames = (pixels / self.thumb.size[0]) + 1
  148.                         self.thumb.size = (pixels/frames, THUMBNAIL_HEIGHT)
  149.                         self.bitmaps = [ (Thumbnail.NOTHING,None) 
  150.                                          for i in range(0,frames) ]
  151.                         self.thumb.bitmaps = self.bitmaps
  152.                         self.g = globals.core.create_thumbnails_generator(file,
  153.                                   s.name,
  154.                                   self.thumb.size[0], self.thumb.size[1],
  155.                                   frames, openvip.DestCallback(None))
  156.                         self.thumb.count = frames
  157.                     self.thumb.ready = True
  158.                     self.__notify()
  159.                     return
  160.  
  161.         def makeBitmap(self, index):
  162.             global thumbsCnt
  163.             thumbsCnt += 1
  164.             # NB: we can't create wxBitmap in non-main thread, so we just
  165.             #     create wxImage (this is MT-safe) and convert it later
  166.             data = self.g.render_single_frame(index)
  167.             img = wxEmptyImage(self.thumb.size[0], self.thumb.size[1])
  168.             img.SetData(data)
  169.             self.thumb.lock.acquire()
  170.             if index < len(self.bitmaps):                
  171.                 self.bitmaps[index] = (Thumbnail.IMAGE,img)
  172.             self.thumb.lock.release()
  173.             if self.mode == MODE_THUMBNAILS or self.mode == MODE_ONE_THUMBNAIL:
  174.                 self.__notify()
  175.  
  176.  
  177.     def __init__(self, obj, mode, zoom, wnds):
  178.         self.lock = threading.Lock()
  179.         self.bitmaps = []
  180.         self.size = None
  181.         self.count = 0
  182.         self.ready = False
  183.         self.generator = Thumbnail.Generator(self, obj, mode, zoom, wnds)
  184.         worker.enqueue(self.generator.run)
  185.         #self.generator.run()
  186.  
  187.     def getBmp(self, index):
  188.         """Return wxBitmap with given index or None if not available yet."""
  189.         if not self.ready:
  190.             return None
  191.         self.lock.acquire()
  192.         flag, data = self.bitmaps[index]
  193.         self.lock.release()
  194.         if flag == Thumbnail.NOTHING:
  195.             self.bitmaps[index] = (Thumbnail.MAKING,None)
  196.             worker.enqueue(self.generator.makeBitmap, index)
  197.             #self.generator.makeBitmap(index)
  198.             return None
  199.         elif flag == Thumbnail.MAKING:
  200.             return None
  201.         elif flag == Thumbnail.IMAGE:
  202.             data = wxBitmapFromImage(data)
  203.             self.bitmaps[index] = (Thumbnail.BITMAP,data)
  204.         return data
  205.  
  206.  
  207. class TrackHeader(wxWindow):
  208.     """List of tracks displayed on the left of timeline widget.
  209.        Implementation detail of TimelineWidget"""
  210.     def __init__(self, parent, widget, tracks, vscroll):
  211.         self.vscroll = vscroll
  212.         self.widget = widget
  213.         self.tracks = tracks
  214.         wxWindow.__init__(self, parent, -1, size=(HEADER_WIDTH,-1))
  215.         self.SetBackgroundColour(HEADER_COLOR)
  216.         EVT_PAINT(self, self.OnPaint)
  217.  
  218.     def OnPaint(self, event):
  219.         size = self.GetSize()
  220.         dc = wxPaintDC(self)
  221.         dc.SetFont(fontNormal)
  222.         dc.SetDeviceOrigin(0, -self.vscroll.GetThumbPosition()*VSCROLL_STEP)
  223.         y = 0
  224.         shift = (TRACK_HEIGHT-dc.GetCharHeight())/2
  225.         for i in self.tracks:
  226.             dc.DrawText(i, 5, y+shift)
  227.             y += TRACK_HEIGHT
  228.             dc.DrawLine(0, y, size.x, y)
  229.  
  230.  
  231. class Ruler(wxWindow):
  232.     """'Ruler' object on top of timeline widget.
  233.        Implementation detail of TimelineWidget"""
  234.     def __init__(self, widget):
  235.         self.widget = widget
  236.         self.selecting = False
  237.         wxWindow.__init__(self, widget, -1, size=(1, RULER_HEIGHT),
  238.                           style=wxNO_FULL_REPAINT_ON_RESIZE)
  239.         
  240.         EVT_PAINT(self, self.OnPaint)
  241.         EVT_LEFT_DOWN(self, self.OnLeftDown)
  242.         EVT_LEFT_UP(self, self.OnLeftUp)
  243.         EVT_MOTION(self, self.OnMouseMove)
  244.  
  245.     def OnNotify(self):
  246.         self.Refresh()
  247.     
  248.     def OnPaint(self, event):
  249.         size = self.GetSize()
  250.         zoom = self.widget.zoom
  251.         origin = self.widget.videopart.origin
  252.         dc = wxPaintDC(self)
  253.         dc.SetFont(fontTiny)
  254.         dc.SetDeviceOrigin(origin[0], 0)
  255.         x = -origin[0]
  256.         t = 0
  257.         cnt = 0
  258.         step = 0.1; bigstep = 1
  259.         if step*zoom < 10: step = 1; bigstep = 10
  260.         if step*zoom < 10: step = 10; bigstep = 60
  261.         if step*zoom < 10: step = 60; bigstep = 5*60
  262.  
  263.         while x < size.x - origin[0]:
  264.             x = int(t * zoom)
  265.             if x >= -origin[0]:
  266.                 if (int(t*100) % (100*bigstep) == 0) or cnt == 0:
  267.                     dc.DrawLine(x, size.y/3, x, size.y-1)
  268.                     dc.DrawText(sec2timecode(t), x, 0)
  269.                     cnt = 0
  270.                 else:
  271.                     dc.DrawLine(x, 2*size.y/3-1, x, size.y-1)
  272.             t += step
  273.             cnt += 1
  274.         
  275.         if self.widget.timeSelection != None:
  276.             dc.SetLogicalFunction(wxINVERT)
  277.             x1 = self.widget.timeSelection[0] * self.widget.zoom
  278.             x2 = self.widget.timeSelection[1] * self.widget.zoom
  279.             dc.DrawRectangle(x1, 0, x2-x1+1, size.y)
  280.             dc.SetLogicalFunction(wxCOPY)
  281.  
  282.     def OnLeftDown(self, event):
  283.         self.selecting = True
  284.         self.selFrom = event.GetX() - self.widget.videopart.origin[0]
  285.         self.selTo = event.GetX() - self.widget.videopart.origin[0]
  286.  
  287.     def OnMouseMove(self, event):
  288.         if self.selecting:
  289.             self.selTo = event.GetX() - self.widget.videopart.origin[0]
  290.         else:
  291.             event.Skip()
  292.             
  293.     def OnLeftUp(self, event):
  294.         if self.selecting:
  295.             if self.selTo > self.selFrom:
  296.                 x1 = self.selFrom
  297.                 x2 = self.selTo
  298.             else:
  299.                 x1 = self.selTo
  300.                 x2 = self.selFrom
  301.             z = self.widget.zoom
  302.             if x2 - x1 >= 2:
  303.                 self.widget.timeSelection = (x1/z, x2/z)
  304.             else:
  305.                 self.widget.timeSelection = None
  306.             self.widget.NotifyWatchers()
  307.             self.selecting = False
  308.         
  309.         # Current position indicator:
  310.         self.widget.SetPosition(float(event.GetX() - 
  311.                           self.widget.videopart.origin[0]) / self.widget.zoom)
  312.         self.widget.NotifyWatchers()
  313.  
  314. class DraggingInfo:
  315.     pass
  316.  
  317. CURSOR_CUT = 'CURSOR_CUT'
  318. CURSOR_ADD = 'CURSOR_ADD'
  319. CURSOR_CANT_ADD = 'CURSOR_CANT_ADD'
  320. class Part(wxWindow):
  321.     """The main part timeline widget, where objects are shown. Part is actually
  322.        half of the widget: either video-only (top) or audio-only (bottom)
  323.        part of it. It handles all user input.
  324.        Implementation detail of TimelineWidget"""
  325.     bmpArrowUp = None
  326.     bmpArrowDown = None
  327.  
  328.     def __init__(self, parent, widget, tracks, vscroll):
  329.         if Part.bmpArrowUp == None:
  330.             Part.bmpArrowUp = wxBitmap('bitmaps/arrow_up.png')
  331.             Part.bmpArrowDown = wxBitmap('bitmaps/arrow_down.png')
  332.             Part.cursors = {
  333.                              wxCURSOR_ARROW:  wxStockCursor(wxCURSOR_ARROW),
  334.                              wxCURSOR_SIZEWE: wxStockCursor(wxCURSOR_SIZEWE),
  335.                              CURSOR_CUT:      widget.cursorCut,
  336.                              CURSOR_ADD:      widget.cursorAdd,
  337.                              CURSOR_CANT_ADD: widget.cursorCantAdd,
  338.                            }
  339.         self.origin = (0,0)
  340.         self.cursor = wxCURSOR_ARROW
  341.         self.mouseMoved = None
  342.         self.vscroll = vscroll
  343.         self.widget = widget
  344.         self.tracks = tracks
  345.         self.backbuffer = None
  346.         self.dragging = None
  347.         self.resizing = None
  348.         wxWindow.__init__(self, parent, -1, style=wxNO_FULL_REPAINT_ON_RESIZE)
  349.         self.SetFont(fontNormal)
  350.         self.SetBackgroundColour(BACKGROUND_COLOR)
  351.         self.objBrush = wxBrush(wxWHITE, wxSOLID)
  352.         self.AdjustVScrollbar()
  353.         
  354.         EVT_PAINT(self, self.OnPaint)
  355.         EVT_UPDATE_THUMBNAILS(self, self.OnUpdateThumbnails)
  356.         EVT_SIZE(self, self.OnSize)
  357.         EVT_IDLE(self, self.OnIdle)
  358.         EVT_LEFT_DOWN(self, self.OnLeftDown)
  359.         EVT_LEFT_UP(self, self.OnLeftUp)
  360.         EVT_RIGHT_DOWN(self, self.OnRightDown)
  361.         EVT_MOTION(self, self.OnMouseMove)
  362.         EVT_ERASE_BACKGROUND(self, self.OnEraseBackground)
  363.  
  364.     def OnEraseBackground(self, event):
  365.         pass
  366.     
  367.     def OnUpdateThumbnails(self, event):
  368.         self.Refresh(False)
  369.     
  370.     def OnNotify(self):
  371.         self.Refresh(False)
  372.  
  373.     def OnSize(self, event):
  374.         self.backbuffer = None
  375.         self.AdjustVScrollbar()
  376.         event.Skip(True)
  377.  
  378.     def CalcObjsPos(self, objects=None, transitions=None):
  379.         self.objPos = {}
  380.         y = 0
  381.         if objects == None: objects = self.widget.model.objects
  382.         if transitions == None: transitions = self.widget.model.transitions
  383.         def calcPos(obj, y, zoom):
  384.             tfrom = int(obj.time_from * zoom)
  385.             tto = int(obj.time_to * zoom)
  386.             return (tfrom, y, tto - tfrom + 1, TRACK_HEIGHT+1)
  387.         for i in self.tracks:
  388.             for obj in [o for o in objects if o.track==i]:
  389.                 self.objPos[obj] = calcPos(obj, y, self.widget.zoom)
  390.             if i == 'VFx' or i == 'AFx':
  391.                 for obj in [o for o in transitions if o.track==i]:
  392.                     self.objPos[obj] = calcPos(obj, y, self.widget.zoom)
  393.             y += TRACK_HEIGHT
  394.  
  395.     def CalcOrigin(self):
  396.         self.origin = (-self.widget.hscroll.GetThumbPosition()*HSCROLL_STEP,
  397.                        -self.vscroll.GetThumbPosition()*VSCROLL_STEP)
  398.  
  399.     def OnPaint(self, event):
  400.         mod = self.widget.model
  401.         size = self.GetSize()
  402.         dcw = wxPaintDC(self)
  403.         if mod == None or (size.x <= 0 or size.y <= 0):
  404.             return;
  405.  
  406.         if thumbsCnt > MAX_THUMBS:
  407.             self.widget.CreateThumbnails()
  408.  
  409.         dc = wxMemoryDC()
  410.         if self.backbuffer == None:
  411.             self.backbuffer = wxEmptyBitmap(size.x, size.y)
  412.         dc.SelectObject(self.backbuffer)
  413.         dc.SetBackground(wxBrush(BACKGROUND_COLOR, wxSOLID))
  414.         dc.Clear()
  415.         dc.SetDeviceOrigin(self.origin[0], self.origin[1])
  416.  
  417.         y = 0
  418.         shift = (TRACK_HEIGHT-dc.GetCharHeight())/2
  419.         dc.SetBrush(self.objBrush)
  420.         dc.SetFont(fontNormal)
  421.         for i in self.tracks:
  422.             y += TRACK_HEIGHT
  423.             dc.DrawLine(-self.origin[0], y, -self.origin[0] + size.x, y)
  424.  
  425.         # draw objects:
  426.         for o in self.objPos:
  427.             x,y,w,h = self.objPos[o]
  428.             dc.DrawRectangle(x, y, w, h)
  429.             
  430.             # --- transitions:
  431.             if isinstance(o, model.Transition):
  432.                 ext = dc.GetTextExtent(o.id)
  433.                 if o.direction == 'AB': bmp = Part.bmpArrowDown
  434.                 else: bmp = Part.bmpArrowUp
  435.                 bw = 4 + bmp.GetWidth()
  436.                 dc.DrawBitmap(bmp, x+2, y+2)
  437.                 dc.DrawText(o.id, x+bw+(w-bw-ext[0])/2, y+(h-ext[1])/2)
  438.  
  439.             # --- objects (clips):
  440.             else:
  441.                 wd = self.widget
  442.                 if wd.mode != MODE_FAST:
  443.                     try:
  444.                         bmpCnt = wd.thumbnails[o].count
  445.                     except KeyError:
  446.                         bmpCnt = 0
  447.                 filename = os.path.basename(o.src_spec['filename'])
  448.  
  449.                 if wd.mode == MODE_FAST or bmpCnt == 0:
  450.                     ext = dc.GetTextExtent(filename)
  451.                     dc.DrawText(filename, x + (w-ext[0])/2, y + (h-ext[1])/2)
  452.                 
  453.                 elif wd.mode == MODE_ONE_THUMBNAIL or bmpCnt == 1:
  454.                     if wd.thumbnails[o].ready:
  455.                         ext = dc.GetTextExtent(filename)
  456.                         xofs = 4+wd.thumbnails[o].size[0]
  457.                         dc.DrawText(filename, x + xofs + (w-xofs-ext[0])/2,
  458.                                               y + (h-ext[1])/2)
  459.                         bmp=wd.thumbnails[o].getBmp(0)
  460.                         if bmp != None: dc.DrawBitmap(bmp, x+2, y+2)
  461.                 
  462.                 elif wd.mode == MODE_THUMBNAILS:
  463.                     if wd.thumbnails[o].ready:
  464.                         x2=x+2
  465.                         sz = wd.thumbnails[o].size[0]
  466.                         inc = float(w-4-sz)/(bmpCnt-1)
  467.                         p = 0
  468.                         for bi in range(0, bmpCnt):
  469.                             if x2+sz >= -self.origin[0]:
  470.                                 if x2 > -self.origin[0] + size.x: break
  471.                                 b = wd.thumbnails[o].getBmp(bi)
  472.                                 if b != None: dc.DrawBitmap(b, x2, y+2)
  473.                             p += 1
  474.                             x2 = x+2 + p*inc
  475.  
  476.         # draw selections:
  477.         dc.SetPen(wxTRANSPARENT_PEN)
  478.         dc.SetLogicalFunction(wxINVERT)
  479.         for o in self.widget.selection:
  480.             try:
  481.                 x,y,w,h = self.objPos[o]
  482.                 dc.DrawRectangle(x, y, w, h)
  483.             except KeyError: pass
  484.         if self.widget.timeSelection != None:
  485.             x1 = self.widget.timeSelection[0] * self.widget.zoom
  486.             x2 = self.widget.timeSelection[1] * self.widget.zoom
  487.             dc.DrawRectangle(x1, 0, x2-x1+1, size.y)
  488.         dc.SetLogicalFunction(wxCOPY)
  489.  
  490.         # draw current position indicator:
  491.         if self.widget.position != None:
  492.             x = int(self.widget.position * self.widget.zoom)
  493.             dc.SetPen(wxRED_PEN)
  494.             dc.DrawLine(x, -self.origin[1], x, -self.origin[1] + size.y)
  495.             
  496.         # blit to real dc:
  497.         dc.SetDeviceOrigin(0, 0)
  498.         dcw.Blit(0, 0, size.x, size.y, dc, 0, 0)
  499.         dc = None
  500.  
  501.     def FindObjectAt(self, x, y):
  502.         x -= self.origin[0]
  503.         y -= self.origin[1]
  504.         for obj in self.objPos:
  505.             l,t,w,h = self.objPos[obj]
  506.             if x>=l and x<l+w and y>=t and y<t+h:
  507.                 return obj            
  508.         return None
  509.  
  510.     def OffsetTrack(self, track, dy, recoverFromError=True):
  511.         try:
  512.             index = self.tracks.index(track) + dy
  513.         except ValueError:
  514.             return track
  515.         if index < 0:
  516.             if recoverFromError: index = 0
  517.             else: return None
  518.         if index >= len(self.tracks):
  519.             if recoverFromError: index = len(self.tracks)-1
  520.             else: return None
  521.         return self.tracks[index]
  522.  
  523.     def SnapObjects(self, objects, resizeable=False):
  524.         """Ajust positions of given objects so that they are aligned with
  525.            the rest of objects nearby."""
  526.         for o in objects:
  527.             if o not in self.objPos.keys(): continue
  528.             pf, y, w, h = self.objPos[o]
  529.             pt = pf + w - 1
  530.             for o2 in self.objPos.keys():
  531.                 if o2 in objects: continue
  532.                 pf2, y, w, h = self.objPos[o2]
  533.                 pt2 = pf2 + w - 1
  534.                 if resizeable:
  535.                     if abs(pf - pf2) <= SNAP_TOLERANCE:
  536.                         o.time_from = o2.time_from
  537.                         break
  538.                     elif abs(pf - pt2) <= SNAP_TOLERANCE:
  539.                         o.time_from = o2.time_to
  540.                         break
  541.                     elif abs(pt - pt2) <= SNAP_TOLERANCE:
  542.                         o.time_to = o2.time_to
  543.                         break
  544.                     elif abs(pt - pf2) <= SNAP_TOLERANCE:
  545.                         o.time_to = o2.time_from
  546.                         break
  547.                 else:
  548.                     if abs(pf - pf2) <= SNAP_TOLERANCE:
  549.                         o.move(o2.time_from)
  550.                         break
  551.                     elif abs(pf - pt2) <= SNAP_TOLERANCE:
  552.                         o.move(o2.time_to)
  553.                         break
  554.                     elif abs(pt - pt2) <= SNAP_TOLERANCE:
  555.                         o.move(o2.time_to - o.length())
  556.                         break
  557.                     elif abs(pt - pf2) <= SNAP_TOLERANCE:
  558.                         o.move(o2.time_from - o.length())
  559.                         break
  560.                 
  561.  
  562.     def AdjustDrag(self, objects, dx, dy):
  563.         old_dx = dx
  564.         old_dy = dy
  565.         changing = True
  566.         iter = 10 # prevent
  567.         while changing and iter > 0:
  568.             iter -= 1
  569.             changing = False
  570.             dy = old_dy
  571.             for o in objects:
  572.                 dfrom = o.time_from + dx
  573.                 dto = o.time_to + dx
  574.                 # check for out-of-timeline case:
  575.                 if dfrom < 0:
  576.                     dx = -o.time_from
  577.                     changing = True
  578.                     continue
  579.                 if dy != 0:
  580.                     track = self.OffsetTrack(o.track, dy, False)
  581.                     if track == None:
  582.                         return None
  583.                 else:
  584.                     track = o.track
  585.  
  586.                 # check for colisitions with other objects:
  587.                 obstacles = [x for x in self.widget.model.objects if
  588.                              (x.track == track and x not in objects)]
  589.                 for o2 in obstacles:
  590.                     if not (dto <= o2.time_from or dfrom >= o2.time_to):
  591.                         # snap to left/right:
  592.                         left = abs(dto - o2.time_from)
  593.                         right = abs(dfrom - o2.time_to)
  594.                         if right > left:
  595.                             dx = o2.time_from - o.length() - o.time_from
  596.                         else:
  597.                             dx = o2.time_to - o.time_from
  598.                         if o.time_from + dx < 0:
  599.                             return None # out of timeline, wa can't do anything
  600.                 dy = 0 # only first object moves vertically
  601.                                                     
  602.                     
  603.         if iter == 0: return None # conflict & failed to adjust
  604.         if old_dx == dx: return 0
  605.         return dx - old_dx
  606.  
  607.     def UpdateCursor(self, x, y):
  608.         selobj = None
  609.         if self.widget.mouseMode == MOUSE_MODE_CUT:
  610.             cursor = CURSOR_CUT
  611.         elif self.widget.mouseMode == MOUSE_MODE_ADD:
  612.             time = float(x-self.origin[0])/self.widget.zoom
  613.             yy = y/TRACK_HEIGHT
  614.             if yy >= len(self.tracks): yy = len(self.tracks)-1
  615.             if yy < 0: yy = 0
  616.             track = self.tracks[yy]
  617.             if self.widget.addCallback.veto(time, track):
  618.                 cursor = CURSOR_CANT_ADD
  619.             else:
  620.                 cursor = CURSOR_ADD
  621.         else:
  622.             x -= self.origin[0]
  623.             y -= self.origin[1]
  624.             cursor = wxCURSOR_ARROW
  625.             for obj in self.objPos:
  626.                 l,t,w,h = self.objPos[obj]
  627.                 r = l+w
  628.                 if y>=t and y<t+h and obj.track[1:3] == 'Fx':
  629.                     if ((x >= l-RESIZE_TOLERANCE and x<= l+RESIZE_TOLERANCE) or
  630.                         (x >= r-RESIZE_TOLERANCE and x<= r+RESIZE_TOLERANCE)):
  631.                         cursor = wxCURSOR_SIZEWE
  632.                         selobj = obj
  633.                         break
  634.         if cursor != self.cursor:
  635.             self.cursor = cursor
  636.             self.SetCursor(Part.cursors[cursor])
  637.         return selobj
  638.  
  639.     def RefreshCursor(self):
  640.         pt = self.ScreenToClient(wxGetMousePosition())
  641.         self.UpdateCursor(pt.x, pt.y)
  642.  
  643.     def OnIdle(self, event):
  644.         if self.mouseMoved != None:            
  645.             self.UpdateCursor(self.mouseMoved[0], self.mouseMoved[1])
  646.             self.mouseMoved = None
  647.         event.Skip()
  648.  
  649.     def __FindGroup(self, objs):
  650.         """Returns objects from all groups containing any objects from objs."""
  651.         d = {}
  652.         m = self.widget.model
  653.         for o in objs:
  654.             d[o] = o
  655.             # iterate all groups that contain this object
  656.             for g in [x for x in m.groups if o in x.objects]:
  657.                 for o2 in g.objects:
  658.                     if o2 not in objs: d[o2] = o2
  659.         return d.values()
  660.  
  661.     def __BeginDragging(self, mainObj, objs, mouseX, mouseY):
  662.         self.dragging = DraggingInfo()
  663.         self.dragging.objs = [mainObj] + \
  664.                              [x for x in self.__FindGroup(objs) if x != mainObj]
  665.         self.dragging.from_pos = (mouseX, mouseY/TRACK_HEIGHT)
  666.         self.dragging.to_pos = self.dragging.from_pos
  667.         primary = self.dragging.objs[0].track[0]
  668.         self.dragging.allowY = len([x for x in self.dragging.objs if x.track[0] == primary]) == 1
  669.         if self.dragging.allowY and self.dragging.objs[0].track[1:3] == 'Fx':
  670.             self.dragging.allowY = False
  671.         self.CaptureMouse()
  672.  
  673.     def OnLeftDown(self, event):
  674.         if self.widget.model == None: return
  675.         if self.widget.mouseMode != MOUSE_MODE_NORMAL: return
  676.         if event.ControlDown(): return
  677.  
  678.         # Begin transition resizing:
  679.         if self.cursor == wxCURSOR_SIZEWE:
  680.             obj = self.UpdateCursor(event.GetX(), event.GetY())
  681.             if obj != None:
  682.                 p = event.GetX()-self.origin[0]
  683.                 l = self.objPos[obj][0]
  684.                 r = l + self.objPos[obj][2]
  685.                 if abs(l-p) < abs(r-p): side=0
  686.                 else: side=1
  687.                 self.resizing = (obj, side)
  688.                 self.CaptureMouse()
  689.             return
  690.         
  691.         # Begin dragging:
  692.         obj = self.FindObjectAt(event.GetX(), event.GetY())
  693.         if obj not in self.widget.selection: return
  694.         self.__BeginDragging(obj, self.widget.selection,
  695.                              event.GetX(), event.GetY())
  696.     
  697.     def OnRightDown(self, event):
  698.         if self.widget.model == None:
  699.             event.Skip()
  700.             return
  701.         elif self.widget.mouseMode == MOUSE_MODE_CUT:
  702.             self.widget.CancelCut()
  703.         elif self.widget.mouseMode == MOUSE_MODE_ADD:
  704.             self.widget.CancelAddObject()
  705.         else:
  706.             event.Skip()
  707.  
  708.     def OnMouseMove(self, event):
  709.         if self.widget.model == None: return
  710.         # Resizing transition:
  711.         if self.resizing != None:
  712.             obj, side = self.resizing
  713.             pos = (event.GetX()-self.origin[0])/self.widget.zoom
  714.             if side == 0: obj.time_from = pos
  715.             else:         obj.time_to = pos
  716.             if obj.time_from > obj.time_to:
  717.                 x = obj.time_from
  718.                 obj.time_from = obj.time_to
  719.                 obj.time_to = x
  720.                 self.resizing = (self.resizing[0], 1-self.resizing[1])
  721.             self.CalcObjsPos(transitions=[obj])
  722.             self.Refresh(eraseBackground=False)
  723.             return
  724.  
  725.         # Nothing:
  726.         if self.dragging == None:
  727.             self.mouseMoved = (event.GetX(), event.GetY())
  728.             event.Skip()
  729.             return
  730.  
  731.         # Dragging:
  732.         prev = self.dragging.to_pos
  733.         if not self.dragging.allowY:
  734.             self.dragging.to_pos = (event.GetX(), self.dragging.from_pos[1])
  735.         else:
  736.             y = event.GetY()/TRACK_HEIGHT
  737.             if y >= len(self.tracks): y = len(self.tracks)-1
  738.             if self.tracks[y][1:3] == 'Fx': y = self.dragging.to_pos[1]
  739.             self.dragging.to_pos = (event.GetX(), y)
  740.         
  741.         dx = self.dragging.to_pos[0]-self.dragging.from_pos[0]
  742.         dy = (self.dragging.to_pos[1]-self.dragging.from_pos[1])*TRACK_HEIGHT
  743.  
  744.         adjust = self.AdjustDrag(self.dragging.objs,
  745.                                  float(dx)/self.widget.zoom,
  746.                                  dy/TRACK_HEIGHT)
  747.         if adjust == None:
  748.             self.dragging.to_pos = prev
  749.             return
  750.         if adjust != 0:
  751.             self.dragging.to_pos = (self.dragging.to_pos[0] + 
  752.                                     int(adjust*self.widget.zoom),
  753.                                     self.dragging.to_pos[1])
  754.             dx += int(adjust*self.widget.zoom)
  755.  
  756.         if prev == self.dragging.to_pos:
  757.             return
  758.  
  759.         self.widget.videopart.__PaintDraggedObjs(self.dragging, prev, dx, dy)
  760.         self.widget.audiopart.__PaintDraggedObjs(self.dragging, prev, dx, dy)
  761.  
  762.     def __PaintDraggedObjs(self, dragging, prev, dx, dy):
  763.         dc = wxClientDC(self)
  764.         dc.SetDeviceOrigin(self.origin[0], self.origin[1])
  765.         dc.SetLogicalFunction(wxINVERT)
  766.         dc.SetPen(wxTRANSPARENT_PEN)
  767.         dpx = prev[0]-dragging.from_pos[0]
  768.         dpy = (prev[1]-dragging.from_pos[1])*TRACK_HEIGHT
  769.         for obj in dragging.objs:
  770.             try:
  771.                 x,y,w,h = self.objPos[obj]
  772.                 dc.DrawRectangle(x+dx, y+dy, w, h)
  773.                 dc.DrawRectangle(x+dpx, y+dpy, w, h)
  774.             except KeyError: pass
  775.             dy = dpy = 0 # only first object moves vertically
  776.             
  777.     def OnLeftUp(self, event):
  778.         if self.widget.model == None: return
  779.  
  780.         # CUT mode:
  781.         if self.widget.mouseMode == MOUSE_MODE_CUT:
  782.             time = float(event.GetX()-self.origin[0])/self.widget.zoom
  783.             self.widget._doCut(time)
  784.             return
  785.  
  786.         # ADD mode:
  787.         if self.widget.mouseMode == MOUSE_MODE_ADD:            
  788.             time = float(event.GetX()-self.origin[0])/self.widget.zoom
  789.             y = event.GetY()/TRACK_HEIGHT
  790.             if y >= len(self.tracks): y = len(self.tracks)-1
  791.             track = self.tracks[y]
  792.             if not self.widget.addCallback.veto(time, track):
  793.                 self.widget._endAddMode(time, track)
  794.             return
  795.         
  796.         # transition is being resized:
  797.         if self.resizing != None:
  798.             obj = self.resizing[0]
  799.             self.resizing = None
  800.             self.ReleaseMouse()
  801.             self.CalcObjsPos()
  802.             self.SnapObjects([obj], resizeable=True)
  803.             notify.notify(self.widget.model)
  804.             return
  805.         
  806.         # objects are being dragged:
  807.         if self.dragging != None:
  808.             self.ReleaseMouse()
  809.             if self.dragging.from_pos != self.dragging.to_pos:
  810.                 dx = self.dragging.to_pos[0]-self.dragging.from_pos[0]
  811.                 dy = self.dragging.to_pos[1]-self.dragging.from_pos[1]
  812.                 if dx != 0:
  813.                     for obj in self.dragging.objs:
  814.                         p = obj.time_from + dx/self.widget.zoom
  815.                         if p < 0: p = 0
  816.                         obj.move(p)
  817.                     self.CalcObjsPos()
  818.                     self.SnapObjects(self.widget.selection)
  819.                     notify.notify(self.widget.model)
  820.                 if dy != 0 and self.dragging.allowY:
  821.                     obj = self.widget.selection[0]
  822.                     obj.track = self.OffsetTrack(obj.track, dy)
  823.                     notify.notify(self.widget.model)
  824.             self.dragging = None
  825.             return
  826.             
  827.         # object selection:
  828.         obj = self.FindObjectAt(event.GetX(), event.GetY())
  829.         if obj == None:
  830.             sel = []
  831.         else:
  832.             if event.ControlDown():
  833.                 if obj not in self.widget.selection:
  834.                     sel = self.widget.selection + [obj]
  835.                 else:
  836.                     sel = self.widget.selection[:] # make copy
  837.                     sel.remove(obj)
  838.             else:
  839.                 sel = [obj]
  840.  
  841.         # Current position indicator:
  842.         self.widget.SetPosition(float(event.GetX() - self.origin[0]) / self.widget.zoom)
  843.         if not self.widget.SetSelection(sel):
  844.             self.widget.NotifyWatchers()
  845.  
  846.     def AdjustVScrollbar(self):        
  847.         scroll = self.vscroll
  848.         pos = scroll.GetThumbPosition()
  849.         totsize = (len(self.tracks) * TRACK_HEIGHT) / VSCROLL_STEP
  850.         thumbsize = self.GetSize()[1] / VSCROLL_STEP
  851.         scroll.SetScrollbar(pos, thumbsize, totsize, thumbsize, refresh=True)
  852.  
  853.  
  854. class TimelineWidget(wxPanel):
  855.     """TimelineWidget is central part of OpenVIP GUI. It contains two active
  856.        areas for manipulating video tracks and audio tracks, header with names
  857.        of tracks and ruler that indicates current scrolled position and zoom
  858.        level.
  859.        User can do following actions: select, move, destroy objects, resize
  860.        transitions, select groups of objects and move them, set position in
  861.        time, select time interval."""
  862.  
  863.     def __init__(self, parent, id, size, objectPanel, previewframe=None):
  864.         img = wxImage('bitmaps/cut_cursor.cur')
  865.         self.cursorCut = wxCursorFromImage(img)
  866.         img = wxImage('bitmaps/add_cursor.cur')
  867.         self.cursorAdd = wxCursorFromImage(img)
  868.         img = wxImage('bitmaps/cant_add_cursor.cur')
  869.         self.cursorCantAdd = wxCursorFromImage(img)
  870.         
  871.         self.mouseMode = MOUSE_MODE_NORMAL
  872.         self.objectPanel = objectPanel
  873.         self.previewframe = previewframe
  874.         self.model = None
  875.         self.audiotracks = STD_AUDIO_TRACKS
  876.         self.videotracks = STD_VIDEO_TRACKS
  877.         self.selection = None
  878.         self.timeSelection = None
  879.         self.position = None
  880.         
  881.         self.zoom = wxConfigBase_Get().ReadFloat('/TimelineWidget/zoom', 1.0)
  882.         
  883.         wxPanel.__init__(self, parent, id, size=size)
  884.         self.ruler = Ruler(self)
  885.         self.splitter = \
  886.             wxSplitterWindow(self, -1,
  887.                              style=wxSP_3D|wxSP_BORDER)
  888.         self.video = wxPanel(self.splitter, -1)
  889.         self.audio = wxPanel(self.splitter, -1)
  890.         self.vvscroll = wxScrollBar(self.video, -1, style=wxSB_VERTICAL)
  891.         self.avscroll = wxScrollBar(self.audio, -1, style=wxSB_VERTICAL)
  892.         self.videohdr = TrackHeader(self.video, self, self.videotracks,
  893.                                     self.vvscroll)
  894.         self.videopart = Part(self.video, self, self.videotracks, self.vvscroll)
  895.         self.audiohdr = TrackHeader(self.audio, self, self.audiotracks,
  896.                                     self.avscroll)
  897.         self.audiopart = Part(self.audio, self, self.audiotracks, self.avscroll)
  898.  
  899.         sizer = wxBoxSizer(wxHORIZONTAL)
  900.         sizer.Add(self.videohdr, 0, wxEXPAND)
  901.         sizer.Add(self.videopart, 1, wxEXPAND)
  902.         sizer.Add(self.vvscroll, 0, wxEXPAND)
  903.         self.video.SetSizer(sizer)
  904.         self.video.SetAutoLayout(True)
  905.         sizer = wxBoxSizer(wxHORIZONTAL)
  906.         sizer.Add(self.audiohdr, 0, wxEXPAND)
  907.         sizer.Add(self.audiopart, 1, wxEXPAND)
  908.         sizer.Add(self.avscroll, 0, wxEXPAND)
  909.         self.audio.SetSizer(sizer)
  910.         self.audio.SetAutoLayout(True)
  911.         
  912.         self.splitter.SplitHorizontally(self.video, self.audio,
  913.                                         self.GetSize().y/2)
  914.         self.splitter.SetMinimumPaneSize(20)
  915.         self.hscroll = wxScrollBar(self, -1, style=wxSB_HORIZONTAL)
  916.         if sys.platform == 'unix':
  917.             # workaround a bug in wxGTK
  918.             self.vvscroll.SetSize(self.vvscroll.GetBestSize())
  919.             self.avscroll.SetSize(self.avscroll.GetBestSize())
  920.             self.hscroll.SetSize(self.hscroll.GetBestSize())
  921.         
  922.         sizer = wxBoxSizer(wxVERTICAL)
  923.         hsizer = wxBoxSizer(wxHORIZONTAL)
  924.         hsizer.Add(HEADER_WIDTH, 1)
  925.         hsizer.Add(self.ruler, 1)
  926.         hsizer.Add(self.avscroll.GetSize().x, 1)
  927.         sizer.Add(hsizer, 0, wxEXPAND)
  928.         sizer.Add(self.splitter, 1, wxEXPAND)
  929.         hsizer = wxBoxSizer(wxHORIZONTAL)
  930.         hsizer.Add(HEADER_WIDTH, 1)
  931.         hsizer.Add(self.hscroll, 1)
  932.         hsizer.Add(self.avscroll.GetSize().x, 1)
  933.         sizer.Add(hsizer, 0, wxEXPAND)
  934.         
  935.         self.SetAutoLayout(True)
  936.         self.SetSizer(sizer)
  937.         self.Layout()
  938.         self.AdjustHScrollbar()
  939.  
  940.         self.watchers = [self.videopart.OnNotify,
  941.                          self.audiopart.OnNotify,
  942.                          self.ruler.OnNotify]
  943.  
  944.         EVT_SIZE(self, self.OnSize)
  945.         EVT_SCROLL(self.vvscroll, self.OnScroll)
  946.         EVT_SCROLL(self.avscroll, self.OnScroll)
  947.         EVT_SCROLL(self.hscroll, self.OnScroll)
  948.         
  949.         mode = wxConfigBase_Get().ReadInt('/TimelineWidget/thumb_mode',
  950.                                           MODE_ONE_THUMBNAIL)
  951.         self.SetMode(mode)
  952.  
  953.     def OnScroll(self, event):
  954.         o = event.GetEventObject()
  955.         if o == self.vvscroll:
  956.             self.videopart.Refresh(eraseBackground=False)
  957.             self.videohdr.Refresh()
  958.             self.videopart.CalcOrigin()
  959.         elif o == self.avscroll:
  960.             self.audiopart.Refresh(eraseBackground=False)
  961.             self.audiohdr.Refresh()
  962.             self.audiopart.CalcOrigin()
  963.         elif o == self.hscroll:
  964.             self.videopart.Refresh(eraseBackground=False)
  965.             self.audiopart.Refresh(eraseBackground=False)
  966.             self.videopart.CalcOrigin()
  967.             self.audiopart.CalcOrigin()
  968.             self.ruler.Refresh()
  969.  
  970.     def OnSize(self, event):
  971.         self.splitter.SetSashPosition(event.GetSize().y/2, True)
  972.         self.AdjustHScrollbar()
  973.         event.Skip(True)
  974.  
  975.     def AdjustHScrollbar(self):
  976.         if self.model == None: return
  977.         width = self.videopart.GetSize()[0]
  978.         totwidth = self.model.length()*self.zoom
  979.         if totwidth < width: totwidth = width
  980.         totwidth += width * ADDITIONAL_SPACE_ON_RIGHT
  981.         scroll = self.hscroll
  982.         pos = scroll.GetThumbPosition()
  983.         totsize = totwidth / HSCROLL_STEP
  984.         thumbsize = width / HSCROLL_STEP
  985.         scroll.SetScrollbar(pos, thumbsize, totsize, thumbsize, refresh=True)
  986.  
  987.     def SetModel(self, m):
  988.         """Must be called to set the model that the widget works on."""
  989.         if self.model != None:
  990.             notify.unlisten(self.model, self.OnModelChanged)
  991.         self.model = m
  992.         notify.listen(m, self.OnModelChanged)
  993.         self.selection = [] 
  994.         self.timeSelection = None
  995.         self.OnModelChanged()
  996.         self.CreateThumbnails()
  997.         if self.previewframe != None:
  998.             self.previewframe.SetModel(self.model)
  999.  
  1000.     def SetZoom(self, zoom):
  1001.         """Set zoom level. 'zoom' is float number that is interpreted as
  1002.            number of pixels (horizontal) used to display 1 second
  1003.            of timeline."""
  1004.         center = float(self.hscroll.GetThumbPosition()*HSCROLL_STEP +
  1005.                        self.GetClientWidth()/2)/self.zoom
  1006.         self.zoom = zoom        
  1007.         pos = int(center * self.zoom - self.GetClientWidth()/2)
  1008.         self.OnModelChanged()
  1009.         self.CreateThumbnails()
  1010.         self.hscroll.SetThumbPosition(pos / HSCROLL_STEP)
  1011.         # synthetize event, SetThumbPosition doesn't send it:
  1012.         e=wxScrollEvent()
  1013.         e.SetEventObject(self.hscroll)
  1014.         self.OnScroll(e)
  1015.         wxConfigBase_Get().WriteFloat('/TimelineWidget/zoom', self.zoom)
  1016.  
  1017.     def SetMode(self, mode):
  1018.         """Set thumbnails visualization mode. One of
  1019.             - MODE_FAST (don't show thumbnails)
  1020.             - MODE_ONE_THUMBNAIL (show only single thumbnail and only video)
  1021.             - MODE_THUMBNAILS (show full filmstrip and audio waveforms)"""
  1022.         self.mode = mode
  1023.         self.CreateThumbnails()
  1024.         self.NotifyWatchers()
  1025.         wxConfigBase_Get().WriteInt('/TimelineWidget/thumb_mode', mode)
  1026.  
  1027.     def SetSelection(self, sel):
  1028.         """Sets selection. Selection is list of objects or transitions."""
  1029.         if self.selection != sel:
  1030.             self.selection = sel
  1031.             self.NotifyWatchers()
  1032.             if self.objectPanel != None:
  1033.                 if len(sel) == 1:
  1034.                     self.objectPanel.SetObject(self.model, sel[0])
  1035.                 else:
  1036.                     self.objectPanel.SetObject(self.model, None)
  1037.             return True
  1038.         return False
  1039.  
  1040.     def GetClientWidth(self):
  1041.         """Returns width of client area (Part) in pixels."""
  1042.         return self.videopart.GetSize().x
  1043.  
  1044.     def GetThumbnailHeight(self):
  1045.         """Returns height of thumbnails."""
  1046.         return THUMBNAIL_HEIGHT
  1047.  
  1048.     def GetPosition(self):
  1049.         """Returns position of (in seconds) of current position indicator.
  1050.            May be None."""
  1051.         return self.position
  1052.  
  1053.     def GetTimeSelection(self):
  1054.         """Returns selected time interval (as a tuple, from and to time in
  1055.            seconds or None if no time selection was made by the user."""
  1056.         return self.timeSelection
  1057.     
  1058.     def SetPosition(self, pos):
  1059.         """Sets position in time. See also GetPosition."""
  1060.         self.position = pos
  1061.         if self.previewframe!=None:
  1062.             self.previewframe.SetPosition(self.position)
  1063.     
  1064.     def Cut(self, callback=None):
  1065.         """Switches TimelineWidget into cutting mode. In this mode, clicking
  1066.            causes objects to be cut into two pieces. After the cut operation
  1067.            finishes, 'callback' is called."""
  1068.         self.mouseMode = MOUSE_MODE_CUT
  1069.         self.videopart.RefreshCursor()
  1070.         self.audiopart.RefreshCursor()
  1071.         self.cutCallback = callback
  1072.  
  1073.     def CancelCut(self):
  1074.         """Cancels cut mode. May only be called after Cut()."""
  1075.         self.mouseMode = MOUSE_MODE_NORMAL
  1076.         self.videopart.RefreshCursor()
  1077.         self.audiopart.RefreshCursor()
  1078.  
  1079.     def _doCut(self, time):
  1080.         if self.selection == None:
  1081.             self.model.cut(time)
  1082.         else:
  1083.             self.model.cut(time, self.selection)
  1084.         notify.notify(self.model)
  1085.         if self.cutCallback != None:
  1086.             self.cutCallback()
  1087.         self.CreateThumbnails()
  1088.     
  1089.     def AddObject(self, callback):
  1090.         """Adds object to timeline. 'callback' is class instance that has
  1091.            two methods:
  1092.                add(time, track)  - called when mouse button clicked on position
  1093.                                    'time' (in seconds) on track 'track'
  1094.                veto(time, track) - called when mouse moves, returns True if
  1095.                                    no object can be placed here, False if it
  1096.                                    can be placed.
  1097.            Note that mouse cursor does not identify *exact* position of new
  1098.            object. For instance, if the user clicks on VFx track anywhere
  1099.            where two video clips overlap, a transition that covers full extent
  1100.            of the overlap will be inserted.
  1101.            
  1102.            "To add" means that TimelineWidget switches to ADD mode in which
  1103.            special cursor is displayed. TimelineWidget
  1104.            does not respond as in normal mode. Instead, when some area
  1105.            is clicked, callback.add is called and if mouse move, callback.veto
  1106.            is called."""
  1107.         self.mouseMode = MOUSE_MODE_ADD
  1108.         self.addCallback = callback
  1109.         self.videopart.RefreshCursor()
  1110.         self.audiopart.RefreshCursor()
  1111.  
  1112.     def CancelAddObject(self):
  1113.         """Cancels object adding mode. May only be called after AddObject()."""
  1114.         self.mouseMode = MOUSE_MODE_NORMAL
  1115.         self.videopart.RefreshCursor()
  1116.         self.audiopart.RefreshCursor()
  1117.         self.addCallback = None
  1118.  
  1119.     def _endAddMode(self, time, track):
  1120.         self.mouseMode = MOUSE_MODE_NORMAL
  1121.         self.videopart.RefreshCursor()
  1122.         self.audiopart.RefreshCursor()
  1123.         self.addCallback.add(time, track)
  1124.         self.addCallback = None
  1125.  
  1126.     def OnModelChanged(self):
  1127.         if self.selection != None:
  1128.             allobj = self.model.objects + self.model.transitions
  1129.             self.selection = [x for x in self.selection if x in allobj]
  1130.         self.videopart.CalcObjsPos()
  1131.         self.audiopart.CalcObjsPos()
  1132.         self.AdjustHScrollbar()
  1133.         self.NotifyWatchers()
  1134.         if self.objectPanel != None:
  1135.             if len(self.selection) == 1:
  1136.                 self.objectPanel.SetObject(self.model, self.selection[0])
  1137.             else:
  1138.                 self.objectPanel.SetObject(self.model, None)
  1139.  
  1140.     def NotifyWatchers(self):
  1141.         for func in self.watchers: func()
  1142.  
  1143.     def CreateThumbnails(self):
  1144.         self.thumbnails = {}
  1145.         global thumbsCnt
  1146.         thumbsCnt = 0
  1147.         if self.mode == MODE_FAST or self.model == None: return
  1148.         for o in self.model.objects:
  1149.             self.thumbnails[o] = Thumbnail(o, self.mode, self.zoom,
  1150.                                            [self.videopart, self.audiopart])
  1151.     
  1152.  
  1153.  
  1154.  
  1155. # ---------------- testing code -----------------
  1156. if __name__ == '__main__':
  1157.     class TestFrame(wxFrame):
  1158.         def __init__(self):
  1159.             wxFrame.__init__(self, NULL, -1, "TimelineWidget Test")
  1160.             win = TimelineWidget(self, -1, size=(700,400), objectPanel=None)
  1161.             win.SetModel(model.load('test.timeline'))
  1162.             sizer = wxBoxSizer(wxVERTICAL)
  1163.             sizer.Add(win, 1, wxEXPAND)
  1164.             self.SetAutoLayout(True)
  1165.             self.SetSizer(sizer)
  1166.             self.Fit()
  1167.             self.Show(True)
  1168.  
  1169.     class MyApp(wxApp):
  1170.         def OnInit(self):
  1171.             wxInitAllImageHandlers()
  1172.             self.mainFrame = TestFrame()
  1173.             return True
  1174.  
  1175.     app = MyApp(0)
  1176.     app.MainLoop()
  1177.     worker.stop()
  1178.  
  1179.